﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows.Markup;
using System.Xml.Linq;
using Phoenix.Xna.Framework.Reflection;

namespace Phoenix.Xna.Framework.Serialization.Markup
{
    /// <summary>
    /// Converts a CLR Type to and from a XAML namespace/type.
    /// </summary>
    internal static class NamespaceCache
    {
        private static Dictionary<Assembly, List<NamespaceDefinition>> _namespacesByAssembly = new Dictionary<Assembly, List<NamespaceDefinition>>();   // A list of namespace definitions for a particular assembly.
        private static Dictionary<NamespaceDefinition, Assembly> _assembliesByNamespace = new Dictionary<NamespaceDefinition, Assembly>();              // A list of assemblys for a particular namespace definition.
        private static bool _allDllsAreLoaded;                                                                                                          // A value indicating all dlls have been loaded.

        /// <summary>
        /// Get the XNamespace for any given type.
        /// </summary>
        /// <param name="type">The System.Type to get the namespace for.</param>
        /// <returns>The XNamespace for the supplied System.Type.</returns>
        public static XNamespace GetNamespaceFor(Type type)
        {
            Assembly assembly = type.Assembly;
            XNamespace xmlNamespace = null;
            List<NamespaceDefinition> namespacesInAssembly = GetNamespacesFor(assembly);
            NamespaceDefinition definition = namespacesInAssembly.FirstOrDefault(nsdef => string.Compare(nsdef.ClrNamespace, type.Namespace, StringComparison.Ordinal) == 0);

            if (definition != null)
            {
                xmlNamespace = definition.XmlNamespace;
            }

            if (xmlNamespace == null)
            {
                xmlNamespace = XNamespace.Get(string.Format(CultureInfo.InvariantCulture, "clr-namespace:{0};assembly={1}", type.Namespace, GetAssemblyName(assembly)));
                AddNamespaceToCache(assembly, new NamespaceDefinition()
                {
                    Assembly = assembly,
                    XmlNamespace = xmlNamespace,
                    ClrNamespace = type.Namespace
                });
            }

            return xmlNamespace;
        }                                                                                                         

        /// <summary>
        /// Get the CLR Type defined by a typename and a XNamespace.
        /// </summary>
        /// <param name="typeName">The name of the type to get.</param>
        /// <param name="xmlNamespace">The namespace the type is defined in.</param>
        public static Type GetType(string typeName, XNamespace xmlNamespace)
        {
            Type desiredType = FindType(typeName, xmlNamespace.NamespaceName, _assembliesByNamespace.Keys.ToList());

            if (desiredType == null)
            {
                string assemblyName = null;
                string clrNamespace = null;
                bool assemblyNameKnown = TryParseClrNamespace(xmlNamespace, out assemblyName, out clrNamespace);
                LoadAllDlls(assemblyName);

                foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    desiredType = FindType(typeName, xmlNamespace.NamespaceName, GetNamespacesFor(assembly));

                    if (desiredType != null)
                    {
                        break;
                    }

                    if (assemblyNameKnown && assembly.FullName.Contains(assemblyName))
                    {
                        desiredType = ResolveType(assembly, clrNamespace, typeName);
                        if (desiredType != null)
                        {
                            AddNamespaceToCache(assembly, new NamespaceDefinition()
                            {
                                Assembly = assembly,
                                XmlNamespace = xmlNamespace,
                                ClrNamespace = clrNamespace
                            });
                            break;
                        }
                    }
                }
            }

            return desiredType;
        }

        private static List<NamespaceDefinition> GetNamespacesFor(Assembly assembly)
        {
            if (!_namespacesByAssembly.ContainsKey(assembly))
            {
                _namespacesByAssembly.Add(assembly, new List<NamespaceDefinition>());
                List<XmlnsDefinitionAttribute> definitionAttributes = assembly.GetAllAttributeOfType<XmlnsDefinitionAttribute>(true);

                foreach (XmlnsDefinitionAttribute definition in definitionAttributes)
                {
                    AddNamespaceToCache(assembly, new NamespaceDefinition()
                    {
                        Assembly = assembly,
                        XmlNamespace = XNamespace.Get(definition.XmlNamespace),
                        ClrNamespace = definition.ClrNamespace
                    });
                }
            }

            return _namespacesByAssembly[assembly];
        }

        private static void AddNamespaceToCache(Assembly assembly, NamespaceDefinition namespaceDefinition)
        {
            _assembliesByNamespace.Add(namespaceDefinition, assembly);
            _namespacesByAssembly[assembly].Add(namespaceDefinition);
        }

        private static string GetAssemblyName(Assembly assembly)
        {
            return assembly.FullName.Substring(0, assembly.FullName.IndexOf(','));
        }

        private static Type FindType(string typeName, string namespaceName, List<NamespaceDefinition> definitions)
        {
            NamespaceDefinition definition = definitions.FirstOrDefault(nsDef => string.Compare(nsDef.XmlNamespace.NamespaceName, namespaceName, StringComparison.Ordinal) == 0);

            if (definition != null)
            {
                return ResolveType(definition.Assembly, definition.ClrNamespace, typeName);
            }

            return null;
        }

        private static Type ResolveType(Assembly assembly, string clrNamespace, string typeName)
        {
            string fullTypeName = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", clrNamespace, typeName);
            return assembly.GetType(fullTypeName);
        }

        private static bool TryParseClrNamespace(XNamespace xmlNamespace, out string assembly, out string clrNamespace)
        {
            string namespaceName = xmlNamespace.NamespaceName;

            if (namespaceName.StartsWith("clr-namespace:", StringComparison.OrdinalIgnoreCase))
            {
                string[] parsed = namespaceName.Split(';');

                if (parsed.Length == 2)
                {
                    clrNamespace = parsed[0].Replace("clr-namespace:", string.Empty);
                    assembly = parsed[1].Replace("assembly=", string.Empty);
                    return true;
                }
            }

            assembly = string.Empty;
            clrNamespace = string.Empty;
            return false;
        }

        private static void LoadAllDlls(string desiredAssembly)
        {
            if (!_allDllsAreLoaded)
            {
                _allDllsAreLoaded = true;
                string localDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
                List<Assembly> loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();

                foreach (string file in Directory.GetFiles(localDirectory, "*.dll"))
                {
                    if (!loadedAssemblies.Exists(assembly => string.Compare(assembly.Location, file, StringComparison.Ordinal) == 0))
                    {
                        Assembly assembly = Assembly.LoadFile(file);

                        if (!string.IsNullOrEmpty(desiredAssembly) && assembly.FullName.Contains(desiredAssembly))
                        {
                            _allDllsAreLoaded = false;
                            break;
                        }
                    }
                }
            }
        }
    }
}

